use std::{
    collections::{HashMap, HashSet},
    path::Path,
};

use anyhow::Ok;
use base64::{Engine, prelude::BASE64_STANDARD};
use chrono::{Duration, Local, Utc};
use hmac::Mac;
use sea_orm::{
    ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, JoinType, QueryFilter,
    QuerySelect, prelude::Expr, sea_query::Asterisk,
};

use crate::{
    entity,
    model::{self, FileSearchParam},
};

pub async fn create(
    db: &DatabaseConnection,
    bucket_id: i64,
    file_key: &str,
    source_file: &str,
    source_file_size: i64,
    source_file_type: &str,
    source_file_attr: &str,
) -> anyhow::Result<i64> {
    let now = Local::now().naive_local();

    let entity = entity::file::ActiveModel {
        id: Set(0),
        bucket_id: Set(bucket_id),
        file_key: Set(file_key.to_string()),
        source_file: Set(source_file.to_string()),
        source_file_size: Set(source_file_size),
        source_file_type: Set(source_file_type.to_string()),
        source_file_attr: Set(source_file_attr.to_string()),
        del_status: Set(0),
        create_time: Set(now),
        update_time: Set(now),
    };

    let id = entity::file::Entity::insert(entity)
        .exec(db)
        .await?
        .last_insert_id;

    Ok(id)
}

pub async fn find_by_id(db: &DatabaseConnection, id: i64) -> anyhow::Result<Option<model::File>> {
    if let Some(entity) = entity::file::Entity::find_by_id(id).one(db).await? {
        let bucket = entity::bucket::Entity::find_by_id(entity.bucket_id)
            .one(db)
            .await?
            .unwrap();
        let app = entity::app::Entity::find_by_id(bucket.app_id)
            .one(db)
            .await?
            .unwrap();

        let mut file = model::File::from(&entity);

        let bucket_name = bucket.name.as_str();
        let domain = bucket.domain.as_str();

        file.bucket_name = bucket_name.to_string();
        file.domain = domain.to_string();
        file.url = get_url(
            &app.access_key_id,
            &app.access_key_secret,
            bucket_name,
            bucket.bucket_type,
            domain,
            &entity.file_key,
            3600,
        );

        return Ok(Some(file));
    }

    Ok(None)
}

pub async fn search(
    db: &DatabaseConnection,
    search_param: &model::FileSearchParam,
) -> anyhow::Result<Vec<model::File>> {
    if search_param.is_empty() {
        return Ok(Vec::<model::File>::with_capacity(0));
    }

    let mut query_builder = entity::file::Entity::find();

    if let Some(ids) = &search_param.ids
        && !ids.is_empty()
    {
        query_builder = query_builder.filter(entity::file::Column::Id.is_in(ids.to_vec()));
    }
    if let Some(buckets) = &search_param.buckets
        && !buckets.is_empty()
    {
        query_builder = query_builder.join(
            JoinType::InnerJoin,
            entity::file::Entity::belongs_to(entity::bucket::Entity)
                .from(entity::file::Column::BucketId)
                .to(entity::bucket::Column::Id)
                .into(),
        );
        query_builder = query_builder.filter(entity::bucket::Column::Name.is_in(buckets.to_vec()));
    }
    if let Some(file_keys) = &search_param.file_keys
        && !file_keys.is_empty()
    {
        query_builder =
            query_builder.filter(entity::file::Column::FileKey.is_in(file_keys.to_vec()));
    }

    let entities = query_builder.all(db).await.unwrap();
    let mut files = Vec::<model::File>::with_capacity(entities.len());

    merge_files(db, search_param, entities, &mut files).await?;

    Ok(files)
}

pub async fn search_page(
    db: &DatabaseConnection,
    search_param: &model::FileSearchPageParam,
) -> anyhow::Result<model::PageResult<model::File>> {
    let mut count_builder = entity::file::Entity::find();
    let mut query_builder = entity::file::Entity::find();

    if let Some(ids) = &search_param.file_search_param.ids
        && !ids.is_empty()
    {
        count_builder = count_builder.filter(entity::file::Column::Id.is_in(ids.to_vec()));
        query_builder = query_builder.filter(entity::file::Column::Id.is_in(ids.to_vec()));
    }
    if let Some(buckets) = &search_param.file_search_param.buckets
        && !buckets.is_empty()
    {
        count_builder = count_builder.join(
            JoinType::InnerJoin,
            entity::file::Entity::belongs_to(entity::bucket::Entity)
                .from(entity::file::Column::BucketId)
                .to(entity::bucket::Column::Id)
                .into(),
        );
        count_builder = count_builder.filter(entity::bucket::Column::Name.is_in(buckets.to_vec()));
        query_builder = query_builder.join(
            JoinType::InnerJoin,
            entity::file::Entity::belongs_to(entity::bucket::Entity)
                .from(entity::file::Column::BucketId)
                .to(entity::bucket::Column::Id)
                .into(),
        );
        query_builder = query_builder.filter(entity::bucket::Column::Name.is_in(buckets.to_vec()));
    }
    if let Some(file_keys) = &search_param.file_search_param.file_keys
        && !file_keys.is_empty()
    {
        count_builder =
            count_builder.filter(entity::file::Column::FileKey.is_in(file_keys.to_vec()));
        query_builder =
            query_builder.filter(entity::file::Column::FileKey.is_in(file_keys.to_vec()));
    }

    let total = count_builder
        .select_only()
        .column_as(Expr::col(Asterisk).count(), "count")
        .into_tuple::<i64>()
        .one(db)
        .await?
        .unwrap_or_default();

    if total > 0 {
        let start = search_param.page_param.start() as u64;
        let page_size = search_param.page_param.page_size as u64;
        let entities = query_builder.offset(start).limit(page_size).all(db).await?;
        let mut files = Vec::<model::File>::with_capacity(entities.len());

        merge_files(db, &search_param.file_search_param, entities, &mut files).await?;

        let mut page_result = model::PageResult::<model::File>::with_data(total, files);

        page_result.total_pages(search_param.page_param.page_size);

        return Ok(page_result);
    }

    Ok(model::PageResult::default())
}

pub fn generate_file_key(upload_file: &model::UploadFile) -> String {
    let mut sb = String::new();

    if let Some(file_key) = &upload_file.file_key
        && !file_key.is_empty()
    {
        sb.push_str(&file_key.to_lowercase());
    } else {
        if let Some(prefix) = &upload_file.prefix
            && !prefix.is_empty()
        {
            sb.push_str(&prefix.to_lowercase());
            sb.push_str("/");
        }

        if let Some(use_source_file_name) = upload_file.use_source_filename
            && use_source_file_name == 1
        {
            if let Some(source_file) = &upload_file.source_file
                && !source_file.is_empty()
            {
                sb.push_str(&source_file)
            }
        } else {
            sb.push_str(bson::oid::ObjectId::new().to_hex().as_str());

            if let Some(source_file) = &upload_file.source_file
                && !source_file.is_empty()
            {
                if let Some(file_ext) = Path::new(&source_file).extension() {
                    sb.push_str(".");
                    sb.push_str(file_ext.to_str().unwrap_or_default());
                }
            }
        }
    }

    sb
}

pub fn get_url(
    access_key_id: &str,
    access_key_secret: &str,
    bucket_name: &str,
    bucket_type: i32,
    bucket_domain: &str,
    file_key: &str,
    expired_in_sec: i64,
) -> String {
    let mut sb = String::new();

    sb.push_str("//");
    sb.push_str(bucket_domain);
    sb.push_str("/");
    sb.push_str(file_key);

    if bucket_type == 2 {
        let expiration = (Utc::now() + Duration::seconds(expired_in_sec)).timestamp();
        let resource = format!("/{}/{}", bucket_name, file_key);
        let sign_str = format!("GET\n{}\n{}\n{}\n{}{}", "", "", expiration, "", resource);

        let mut mac =
            hmac::Hmac::<sha1::Sha1>::new_from_slice(access_key_secret.as_bytes()).unwrap();

        mac.update(sign_str.as_bytes());

        let signed_str: String = url::form_urlencoded::byte_serialize(
            BASE64_STANDARD
                .encode(mac.finalize().into_bytes())
                .as_bytes(),
        )
        .collect();

        sb.push_str("?");
        sb.push_str(
            format!(
                "Expires={}&OSSAccessKeyId={}&Signature={}",
                expiration, access_key_id, signed_str
            )
            .as_str(),
        );
    }

    sb
}

async fn merge_files(
    db: &DatabaseConnection,
    search_param: &FileSearchParam,
    entities: Vec<entity::file::Model>,
    files: &mut Vec<model::File>,
) -> anyhow::Result<()> {
    if !entities.is_empty() {
        let mut bucket_id_set = HashSet::<i64>::with_capacity(entities.len());

        for entity in entities.iter() {
            bucket_id_set.insert(entity.bucket_id);
        }

        let bucket_ids: Vec<i64> = bucket_id_set.into_iter().collect();

        let buckets = entity::bucket::Entity::find()
            .columns([
                entity::bucket::Column::Id,
                entity::bucket::Column::AppId,
                entity::bucket::Column::BucketType,
                entity::bucket::Column::Name,
                entity::bucket::Column::Domain,
            ])
            .filter(entity::bucket::Column::Id.is_in(bucket_ids))
            .all(db)
            .await?;

        let mut bucket_map = HashMap::<i64, &entity::bucket::Model>::with_capacity(buckets.len());
        let mut app_id_set = HashSet::<i64>::with_capacity(buckets.len());

        for bucket in buckets.iter() {
            bucket_map.insert(bucket.id, bucket);

            app_id_set.insert(bucket.app_id);
        }

        let app_ids: Vec<i64> = app_id_set.into_iter().collect();

        let app_map: HashMap<i64, entity::app::Model> = entity::app::Entity::find()
            .columns([
                entity::app::Column::Id,
                entity::app::Column::AccessKeyId,
                entity::app::Column::AccessKeySecret,
            ])
            .filter(entity::app::Column::Id.is_in(app_ids))
            .all(db)
            .await?
            .into_iter()
            .map(|app| (app.id, app))
            .collect();

        let expired_in_sec = search_param.expired_in_sec.unwrap_or_else(|| 3600);

        for entity in entities.iter() {
            let mut file = model::File::from(entity);

            if let Some(bucket) = bucket_map.get(&file.bucket_id) {
                file.bucket_name = bucket.name.clone();
                file.domain = bucket.domain.clone();

                if let Some(app) = app_map.get(&bucket.app_id) {
                    file.url = get_url(
                        &app.access_key_id,
                        &app.access_key_secret,
                        &bucket.name,
                        bucket.bucket_type,
                        &bucket.domain,
                        &file.file_key,
                        expired_in_sec,
                    );
                }
            }

            files.push(file);
        }
    }

    Ok(())
}
